import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { AlertCircle, XIcon } from 'lucide-react' import Link from 'next/link' import { useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { Alert, AlertDescription, AlertTitle, Button } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { useFeaturePreviewModal, useIsColumnLevelPrivilegesEnabled, } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { getDefaultColumnCheckedStates, getDefaultTableCheckedStates, useApplyPrivilegeOperations, usePrivilegesState, } from '@/components/interfaces/Database/Privileges/Privileges.utils' import PrivilegesHead from '@/components/interfaces/Database/Privileges/PrivilegesHead' import PrivilegesTable from '@/components/interfaces/Database/Privileges/PrivilegesTable' import { ProtectedSchemaWarning } from '@/components/interfaces/Database/ProtectedSchemaWarning' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from '@/components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import AlertError from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge' import { PgRole, useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { useColumnPrivilegesQuery } from '@/data/privileges/column-privileges-query' import { useTablePrivilegesQuery } from '@/data/privileges/table-privileges-query' import { useTablesQuery } from '@/data/tables/tables-query' import { useLocalStorage } from '@/hooks/misc/useLocalStorage' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas' import { DOCS_URL } from '@/lib/constants' import type { NextPageWithLayout } from '@/types' const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role'] const PrivilegesPage: NextPageWithLayout = () => { const { ref, table: paramTable } = useParams() const { data: project } = useSelectedProjectQuery() const { toggleFeaturePreviewModal } = useFeaturePreviewModal() const isEnabled = useIsColumnLevelPrivilegesEnabled() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() const [selectedTable, setSelectedTable] = useState(paramTable) const [selectedRole, setSelectedRole] = useState('authenticated') const { data: tableList, isPending: isLoadingTables, isSuccess: isSuccessTables, } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) useEffect(() => { if (!isSuccessTables) return const tables = tableList .filter((table) => table.schema === selectedSchema) .map((table) => table.name) if (tables[0] && selectedTable === undefined) { setSelectedTable(tables[0]) } }, [isSuccessTables, tableList, selectedSchema, selectedTable]) const { data: allRoles, isPending: isLoadingRoles } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const tables = tableList ?.filter((table) => table.schema === selectedSchema) .map((table) => table.name) const { data: allTablePrivileges, isPending: isLoadingTablePrivileges, isError: isErrorTablePrivileges, error: errorTablePrivileges, } = useTablePrivilegesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const tablePrivilege = useMemo(() => { const tablePrivilege = allTablePrivileges?.find( (tablePrivilege) => tablePrivilege.schema === selectedSchema && tablePrivilege.name === selectedTable ) if (tablePrivilege) { return { ...tablePrivilege, privileges: tablePrivilege.privileges.filter( (privilege) => privilege.grantee === selectedRole ), } } }, [allTablePrivileges, selectedRole, selectedSchema, selectedTable]) const { data: allColumnPrivileges, isPending: isLoadingColumnPrivileges, isError: isErrorColumnPrivileges, error: errorColumnPrivileges, } = useColumnPrivilegesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const columnPrivileges = useMemo( () => allColumnPrivileges ?.filter( (privilege) => privilege.relation_schema === selectedSchema && privilege.relation_name === selectedTable ) .map((privilege) => ({ ...privilege, privileges: privilege.privileges.filter( (privilege) => privilege.grantee === selectedRole ), })) ?? [], [allColumnPrivileges, selectedRole, selectedSchema, selectedTable] ) const rolesList = allRoles?.filter((role: PgRole) => EDITABLE_ROLES.includes(role.name)) ?? [] const roles = rolesList.map((role: PgRole) => role.name) const table = tableList?.find( (table) => table.schema === selectedSchema && table.name === selectedTable ) const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) const { tableCheckedStates, columnCheckedStates, toggleTablePrivilege, toggleColumnPrivilege, operations, resetOperations, } = usePrivilegesState( useMemo( () => ({ tableId: table?.id ?? -1, role: selectedRole, defaultTableCheckedStates: tablePrivilege ? getDefaultTableCheckedStates(tablePrivilege) : {}, defaultColumnCheckedStates: getDefaultColumnCheckedStates(columnPrivileges), }), [columnPrivileges, selectedRole, table?.id, tablePrivilege] ) ) const hasChanges = operations.length > 0 const handleChangeSchema = (schema: string) => { if (hasChanges) { if (window.confirm('You will lose your changes. Are you sure?')) { resetOperations() } else { return } } const newTable = tableList?.find((table) => table.schema === schema)?.name setSelectedSchema(schema) setSelectedTable(newTable) } const handleChangeTable = (table: string) => { if (hasChanges) { if (window.confirm('You will lose your changes. Are you sure?')) { resetOperations() } else { return } } setSelectedTable(table) } const handleChangeRole = (role: string) => { setSelectedRole(role) } const { apply: applyColumnPrivileges, isLoading: isApplyingChanges } = useApplyPrivilegeOperations( useCallback(() => { toast.success( `Successfully updated privileges on ${selectedSchema}.${selectedTable} for ${selectedRole}`, { duration: 6000 } ) resetOperations() }, [resetOperations, selectedRole, selectedSchema, selectedTable]) ) function applyChanges() { applyColumnPrivileges(operations) } const [diffWarningDismissed, setDiffWarningDismissed] = useLocalStorage( LOCAL_STORAGE_KEYS.CLS_DIFF_WARNING, false ) const [selectStarWarningDismissed, setSelectStarWarningDismissed] = useLocalStorage( LOCAL_STORAGE_KEYS.CLS_SELECT_STAR_WARNING, false ) const isLoading = isLoadingTablePrivileges || isLoadingColumnPrivileges || isLoadingTables || isLoadingRoles const isError = isErrorTablePrivileges || isErrorColumnPrivileges return (

Column-level privileges

{isEnabled && ( )}

Grant or revoke privileges on a column based on user role.

{isEnabled ? ( <> {!diffWarningDismissed && ( Changes to column privileges will not be reflected in migrations when running{' '} briven db diff. Column privileges are not supported in the current version of the Briven CLI.
You will need to manually apply these changes to your database.
)} {!selectStarWarningDismissed && ( Changing column privileges can break existing queries. If you remove a column privilege for a role, that role will lose all access to that column.
All operations selecting * (including returning * for{' '} insert,{' '} update, and{' '} delete) will fail.
)} {isSchemaLocked && ( )} {isLoading ? ( ) : isError ? ( ) : table && tablePrivilege ? (
) : (tables ?? []).length === 0 ? (

There are no tables in the {selectedSchema} schema

Once a table is available in the schema, you may manage it's column-level privileges here

{selectedSchema === 'public' && ( )}
) : (

Select a table to edit privileges

)} ) : ( Column-level privileges is a dashboard feature preview You may access this feature by enabling it under dashboard feature previews.
)}
) } PrivilegesPage.getLayout = (page) => ( {page} ) export default PrivilegesPage